package com.cellngine.server;

import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;

import com.cellngine.Constants;
import com.cellngine.JO;
import com.cellngine.cellngine;

/**
 * ServerManager
 * 
 * Provides a simple API to configure cellngine's network I/O.
 * 
 * @author Felix Kirchmann <qwer@cellngine.com>
 */
public class ServerManager
{
	private ServerPort						serverPort;
	private InitialKeyPair					keyPair;
	private String							hubURL;
	private HashMap<Integer, ServerClient>	clients;
	private ArrayList<ServerClientSocket>	sockets;
	private int								maxPlayers	= Constants.default_maxplayers;
	private int								currentPort	= Constants.default_port;
	private int								nextPort	= Constants.default_port;
	private String							password	= null;
	
	/**
	 * Creates a new <code>ServerClient</code>, assigns an ID to it and adds it to the internal list
	 * of connected clients.
	 * 
	 * @return The created <code>ServerClient</code>, or <code>null</code> if the creation failed.
	 */
	public synchronized ServerClient createClient()
	{
		final int id = createID();
		if (id < 0)
		{
			cellngine.log("Error: Unable to create ServerClient: maxPlayers reached or exceeded!",
					Constants.log_type.error);
			return null;
		}
		final ServerClient client = new ServerClient(this, id);
		clients.put(id, client);
		return client;
	}
	
	/**
	 * Returns a <code>ServerClient</code> with the given <code>id</code>
	 * 
	 * @param id
	 *            The <code>ServerClient</code>'s ID.
	 * @return A reference to the <code>ServerClient</code>, or <code>null</code> if <code>id</code>
	 *         isn't assigned.
	 */
	public ServerClient getClient(final int id)
	{
		if (clients.containsKey(id))
		{
			return clients.get(id);
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * @return A current snapshot (= a copy) of the list of all connected clients, indexed by their
	 *         ID, or <code>null</code> if the server isn't running.
	 */
	@SuppressWarnings("unchecked")
	public HashMap<Integer, ServerClient> getClients()
	{
		if (clients == null)
		{
			return null;
		}
		return (HashMap<Integer, ServerClient>) clients.clone();
	}
	
	/**
	 * Removes a client from the internal ID list, freeing up the slot.
	 * 
	 * @param id
	 *            The ID of the client which shall be removed. If it is unknown, this function won't
	 *            do anything.
	 */
	public synchronized void removeClient(final int id)
	{
		if (clients.containsKey(id))
		{
			clients.remove(id);
		}
	}
	
	/**
	 * Adds a connection to the internal list.
	 * 
	 * @param connection
	 *            A reference to the connection which shall be added. If it is unknown or
	 *            <code>null</code>, this function won't do anything.
	 */
	public synchronized void addConnection(final ServerClientSocket connection)
	{
		if (connection == null)
		{
			return;
		}
		sockets.add(connection);
	}
	
	/**
	 * @return A current snapshot (= a copy) of the list of all connections, or <code>null</code> if
	 *         the server isn't running.
	 */
	@SuppressWarnings("unchecked")
	public ArrayList<ServerClientSocket> getConnections()
	{
		if (sockets == null)
		{
			return null;
		}
		return (ArrayList<ServerClientSocket>) sockets.clone();
	}
	
	/**
	 * Removes a connection from the internal list.
	 * 
	 * @param connection
	 *            A reference to the connection which shall be removed. If it is unknown or
	 *            <code>null</code>, this function won't do anything.
	 */
	public synchronized void removeConnection(final ServerClientSocket connection)
	{
		if (connection == null)
		{
			return;
		}
		if (sockets.contains(connection))
		{
			sockets.remove(connection);
		}
	}
	
	/**
	 * Enables hub-supported connection security by generating a key pair (if it hasn't already been
	 * generated) and attempting to upload it to the hub. (see the security scheme for details)
	 * 
	 * @param hubURL
	 *            The URL to which the public key will be uploaded - see
	 *            <code>InitialKeyPair.uploadPublicKey</code> for details.
	 * @return Whether or not the key was successfully uploaded. If not, hub-supported connection
	 *         security won't be used.
	 */
	public boolean initializeHub(final String hubURL)
	{
		if (keyPair == null)
		{
			keyPair = new InitialKeyPair();
			keyPair.generateKeyPair();
		}
		return attemptHubKeyUpload(hubURL);
	}
	
	private boolean attemptHubKeyUpload(final String hubURL)
	{
		if (keyPair.uploadPublicKey(hubURL, currentPort))
		{
			this.hubURL = hubURL;
			return true;
		}
		else
		{
			this.hubURL = null;
			return false;
		}
	}
	
	/**
	 * Encrypts or decrypts text using the key uploaded to the hub. If no key has been uploaded yet,
	 * it will always return <code>null</code>.
	 * 
	 * See <code>InitialKeyPair.crypt</code> for details.
	 */
	public byte[] hubCrypt(final byte[] plainText, final int mode) throws InvalidKeyException,
			BadPaddingException, IllegalBlockSizeException, UnsupportedOperationException
	{
		if (!isHubRegistered())
		{
			return null;
		}
		return keyPair.crypt(plainText, mode);
	}
	
	/**
	 * @return Whether hub-supported connection security is used.
	 */
	public boolean isHubRegistered()
	{
		return hubURL != null;
	}
	
	/**
	 * @return The URL to which the public key was uploaded. Will return <code>null</code> if
	 *         hub-supported connection security is disabled.
	 */
	public String hubURL()
	{
		return hubURL;
	}
	
	/**
	 * For thread safety, this function combines starting and stopping the network
	 * socket.
	 * 
	 * @param state
	 *            If <code>true</code>, the server will be started (unless it is already running),
	 *            if <code>false</code>, the server will be stopped.
	 * @throws <code>IllegalArgumentException</code> if state is smaller than 0 or larger than 2.
	 */
	private synchronized void setState(final boolean state)
	{
		if (state)
		{
			if (serverPort != null && serverPort.isActive())
			{
				return;
			}
			clients = new HashMap<Integer, ServerClient>();
			sockets = new ArrayList<ServerClientSocket>();
			if (currentPort != nextPort && hubURL != null)
			{
				// If the port has changed, the hub must be updated accordingly.
				attemptHubKeyUpload(hubURL);
			}
			currentPort = nextPort;
			serverPort = new ServerPort(this, currentPort);
			serverPort.start();
		}
		else
		{
			if (serverPort != null)
			{
				serverPort.stopListening();
				serverPort = null;
			}
			if (clients != null)
			{
				for (final Object key : clients.entrySet())
				{
					clients.get(key).disconnect();
				}
				final Iterator<ServerClientSocket> iter = sockets.iterator();
				while (iter.hasNext())
				{
					iter.next().close();
				}
				clients = null;
				sockets = null;
			}
		}
	}
	
	/**
	 * @return The port number the server is currently listening on. In case the server is not
	 *         active, it will return the port it was listening on before it was stopped.
	 */
	public int getPortCurrent()
	{
		return currentPort;
	}
	
	/**
	 * @return The port number the server will listen on once it's (re-)started.
	 */
	public int getPortNext()
	{
		return nextPort;
	}
	
	/**
	 * Sets the port the server shall listen on once it is (re-)started.
	 * 
	 * @param newPort
	 *            The new port to listen on.
	 * @throws <code>IllegalArgumentException</code> if <code>JO.isPortValid(newPort)</code> returns
	 *         <code>false</code>.
	 */
	public void setPort(final int newPort)
	{
		if (!JO.isPortValid(newPort))
		{
			throw new IllegalArgumentException();
		}
		nextPort = newPort;
	}
	
	/**
	 * @return Returns whether the server is accepting connections on the specified
	 *         <code>port</code>.
	 */
	public boolean isActive()
	{
		return serverPort.isActive();
	}
	
	/**
	 * Causes the server to close all active connections and stop waiting for connections.
	 */
	public void stopListening()
	{
		setState(false);
	}
	
	/**
	 * Starts the server, or does nothing if it is already running.
	 */
	public void start()
	{
		setState(true);
	}
	
	/**
	 * Forces a restart of the network I/O, closing all connections and applying a new port number
	 * given by <code>setPort</code>. If no new port number was given, the previous one will be
	 * reused.
	 */
	public synchronized void restart()
	{
		setState(true);
		setState(false);
	}
	
	/**
	 * Sets the password which is required to connect to the server.
	 * 
	 * @param password
	 *            The new password, set it to null to disable password authentication.
	 */
	public void setPassword(final String password)
	{
		this.password = password;
	}
	
	/**
	 * @return The password which is required to connect to this server, or <code>null</code> if no
	 *         password is required.
	 */
	public String getPasword()
	{
		return password;
	}
	
	/**
	 * Outputs whether a password is required to connect to this server.
	 * 
	 * @return <code>true</code> if a password is required to connect, otherwise <code>false<code>.
	 */
	public boolean passwordEnabled()
	{
		if (password == null)
		{
			return false;
		}
		return true;
	}
	
	/**
	 * @return The maximum amount of players that can simultaneously connect to this server.
	 */
	public int getMaxPlayers()
	{
		return maxPlayers;
	}
	
	/**
	 * @return Whether the Server can't accept further cellngine client connections.
	 */
	public boolean isFull()
	{
		return clients.size() >= maxPlayers;
	}
	
	/**
	 * Sets the maximum amount of players that can simultaneously connect to this server.
	 * 
	 * @param maxPlayers
	 *            The maximum player limit. If the current number of players reaches or exceeds this
	 *            limit, no further client connections will be allowed.
	 * @throws <code>IllegalArgumentException</code> if <code>maxPlayers</code> is smaller than 0 or
	 *         larger than 255
	 */
	public void setMaxPlayers(final int maxPlayers)
	{
		if (maxPlayers < 0 || maxPlayers > 255)
		{
			throw new IllegalArgumentException();
		}
		this.maxPlayers = maxPlayers;
	}
	
	// ---------------------------------------------------
	
	private int createID()
	{
		for (int i = 0; i < maxPlayers; i++)
		{
			if (!clients.containsKey(i))
			{
				return i;
			}
		}
		return -1;
	}
	
}
